<?php
CModule::IncludeModule('iblock');


class IBlockElement implements ArrayAccess{
	// !!! Обязательно в каждом классе переопределяем код инфоблока
	public static $typeCode = null;
	public static $blockCode = null;
	// !!! //
	public $fields;
	public $properties;
	private $sections = null;
	private $link = null;
	private $previewPicSrc = null;
	

	/** Интерфейсная часть
	 * ArrayAccess
	 */
	public function offsetExists($offset){
		return isset($this->fields[$offset]);
	}
	public function offsetGet($offset) {
		return $this->fields[$offset];
	}
	public function offsetSet($offset, $value){
		$this->fields[$offset] = $value;
	}
	public function offsetUnset($offset){
		unset($this->fields[$offset]);
	}
	
	/**
	 * Конструируется либо абстрактный инфоблок с заданным кодом, 
	 * либо конкретный, с помощью класса. 
	 * @param $input string | array | CIBlockResult код произвольного блока либо данные от Битрикса 
	 */
	function __construct($input = null, $fields = null){
		if($input != null){
			// для абстрактного инфоблока оставляем возможность создания произвольного блока,
			// для абстрактного списка. На случай, если класс не написан, или нет смысла писать.
			if(is_string($input)){
				$code = explode('.', $input);
				self::$typeCode = $code[0];
				self::$blockCode = $code[1];
			}else{
				// по ид загружаем из бд 
				if(is_numeric($input)){
					$input = $this->load(intval($input), $fields);
				}
				$this->setUpBitrixResult($input, $fields);
			}
		}
	}
	
	/**
	 * Значение поля 
	 * @param $name
	 */
	public function field($name){
		return $this->fields[$name];
	}
	
	/**
	 * Универсальная добыча значений доп. свойств
	 * @param string $name
	 * @return mixed
	 */
	public function prop($name){
		$ret = null;
		if(isset($this->properties[$name])){
			if(is_array($this->properties[$name]['VALUE'])
				&& isset($this->properties[$name]['VALUE']['TYPE']))
			{	// у некоторых свойств, например типа HTML/текст,
				// значение запрятано глубже
				$ret = $this->properties[$name]['VALUE']['TEXT'];
			}
			else
			{
				$ret = $this->properties[$name]['VALUE'];
			}
		}
		return $ret;
	}
	
	/**
	 * Разбирает результат выборки, полученный Битриксом,
	 * и сохраняет данные элемента. 
	 * @param array | CIBlockResult $result
	 */
	private function setUpBitrixResult($result, $fields = null){
		if($result instanceof CIBlockResult){
			if($row = $result->GetNextElement()){
				$this->fields = $row->getFields();
				// если свойства не заказывали явно - просто получим выбранные автоматом
				if($fields == null){
					$this->properties = $this->extractProperties();
				}
			}
		}
		else{
			if($result instanceof _CIBElement){
				$this->fields = $result->getFields();
				// если свойства не заказывали явно - просто получим выбранные автоматом
				if($fields == null){
					$this->properties = $this->extractProperties();
				}
			}
			else{
				if(is_array($result)){
					if(isset($result['ID'])){
						if(isset($result['PROPERTIES'])){
							$this->properties = $result['PROPERTIES'];
							unset($result['PROPERTIES']);
						}
						if(isset($result['DISPLAY_PROPERTIES'])){
							foreach($result['DISPLAY_PROPERTIES'] as $key=>$prop){
								if(isset($this->properties[$key])){
									$this->properties[$key]['VALUE'] = $prop['DISPLAY_VALUE'];
								}else{
									$this->properties[$key] = $prop;
								}
							}
							unset($result['DISPLAY_PROPERTIES']);
						}
						$this->fields = $result;
					}
				}
			}
		}

		// отдельно прочесываем свойства на предмет возвращенных
		// свойств в виде [PROPERTY_propname.subname]
		if(is_array($fields)){
			foreach($fields as $fieldName){
				$parts = array();
				if(preg_match('/^PROPERTY_(.+)/', $fieldName, $parts)){
					// сначала создаем, если не создано, свойство
					$propName = $parts[1];
					if(!isset($this->properties[$propName])){
						$this->properties[$propName] = array();
					}
					// вычисляем название поля, в котором получено
					// требуемое свойство
					$fieldName = 'PROPERTY_'
						. strtoupper( strtr($propName, '.','_') );
					$this->properties[$propName]['VALUE'] = 
						$this->fields[$fieldName];
					unset($this->fields[$fieldName]);
				}
			}
		}
		$this->initProperties();
	}
	
	private function extractProperties(){
		$ret = array();
		foreach($this->fields as $name => $field){
			$matches = array();
			if(preg_match('/^PROPERTY_(.+)_VALUE$/', $name, $matches)){
				$ret[ $matches[1] ] = array(
					'VALUE'=>$field
				);
				unset($this->fields[$name]);
			}
		}

		return $ret;
	}
	
	/**
	 * После получения данных можно получить произвольные свойства,
	 * на этапе создания объекта
	 */
	public function initProperties(){}
	
	/**
	 * Загрузка инфоблока по ид
	 * @param int $id
	 * @return CIBlockResult
	 */
	public function load($id, $fields = null){
		return CIBlockElement::GetById($id);
		// TODO сделать обработку полей $fields
	}
	
	/**
	 * Выдача ссылки. По умолчанию - ссылка из самого инфоблока 
	 */
	public function getLink(){
		if($this->link == null){
			$this->link = self::buildLink($this->fields['DETAIL_PAGE_URL']);
		}
		return $this->link;
	}
	
	/**
	 * Приведение дефолтной ссылки в надлежащий вид.
	 * Например, переделка в ЧПУ. Можно использовать статически.
	 * @param string $url
	 * @return string
	 */
	public static function buildLink($url){
		return $url;
	}
	
	/**
	 * Картинка-превью
	 */
	public function getPreviewPicSrc(){
		if($this->previewPicSrc == null 
			&& is_numeric($this->field('PREVIEW_PICTURE'))
		){
			$this->previewPicSrc = CFile::GetPath($this->field('PREVIEW_PICTURE'));
		}
		return $this->previewPicSrc;
	}
	
	/**
	 * Массив разделов, к которым привязан элемент
	 */
	public function getSections() {
		if($this->sections == null){
			$result = CIBlockElement::GetElementGroups($this->field('ID'), true);

			while($section = $result->GetNext()){
				$this->sections[] = $section;
			}
		}
		return $this->sections;
	}
	
	/**
	 * Загрузка последнего, самого свежего элемента
	 */
	public static function loadLastOne($blockCode = null){
		$ret = null;
		if($blockCode!=null){
			self::$blockCode = $blockCode;
		}
		$listClass = get_class() . 'List';
		$list = new $listClass();
		$now = date('d-m-Y');
		$result = $list->load(
			array(
				'ACTIVE_DATE'=>$now
			),
			array(
				'DATE_ACTIVE_FROM'=>'DESC',
				'DATE_ACTIVE_TO'=>'ASC'
			),
			1
		);
		if(!empty($result)){
			$ret = $result[0];
		}

		return $ret;
	}

	/**
	 * Формирует RSS-узел
	 * @return string
	 */
	public function toRss($forceUtf = false){
		$date = strtotime($this->fields['DATE_ACTIVE_FROM']);
		$date = date('r', $date);
		$title = $this->fields['NAME'];
		$descr = $this->fields['PREVIEW_TEXT'];
		if($forceUtf){
			$title = mb_convert_encoding($title, 'utf-8', 'cp1251');
			$descr = mb_convert_encoding($descr, 'utf-8', 'cp1251');
		}
		$siteRoot = 'http://'.$_SERVER['SERVER_NAME'];
		$ret = '<item>';
		$ret.= sprintf(
			'<title><![CDATA[%s]]></title>', $title
		);
		$ret.= sprintf(
			'<description><![CDATA[%s]]></description>', $descr
		);
		$ret.= sprintf('<link>%s</link>', $siteRoot . $this->getLink());
		$ret.= sprintf('<pubDate>%s</pubDate>', $date);
		$ret.= '</item>';
		return $ret;
	}

}

/**
 * Класс, упрощающий загрузку инфоблоков и их элементов
 * Представляет "массив" элементов инфоблоков 
 * @author Konev N.
 */
class IBlockElementList 
implements Iterator, ArrayAccess, Countable, Serializable {
	private static $ELEMENT_CLASS = 'IBlockElement'; // Дефолтное значение
	private $list = array();
	private $position = 0; 
	public static $typeCode = null;
	public static $blockCode = null;
	public $active = 'Y';
	
	/**
	 * Интерфейсная часть
	 * Iterator 
	 */
	public function current(){
		return $this->list[$this->position];
	}
	public function rewind(){
		$this->position = 0;
	}
	public function key(){
		return $this->position;
	}
	public function next(){
		++$this->position;
	}
	public function valid(){
		return isset($this->list[$this->position]);
	}
	
	/**
	 * ArrayAccess
	 */
	public function offsetExists($offset){
		return isset($this->list[$offset]);
	}
	public function offsetGet($offset) {
		return $this->list[$offset];
	}
	public function offsetSet($offset, $value){
		$this->list[$offset] = $value;
	}
	public function offsetUnset($offset){
		unset($this->list[$offset]);
	}
    /**
     * Serialaizable
     */
    public function serialize() {
    	return serialize($this->list);
    }
    public function unserialize($value) {
    	$this->list = unserialize($value);
    }
    /**
     * Countable
     */
    public function count() {
    	return count($this->list);
    }
    
    /**
     * Доп. функции
     */
    public function add($item){
    	if($item instanceof self::$ELEMENT_CLASS){
	    	$this->list[] = $item;
    	}
    	else{
    		$this->list[] = new self::$ELEMENT_CLASS($item);
    	}
    }

    /**
     * Вернет указанный элемент списка.
     * Позволяет вызывать цепочки, типа $list->item(23)->prop('HEAD');
     * @param $index
     */
    public function item($index){
    	return $this->list[$index];
    }
    
    /**
     * Удаляет указанный элемент. Если не указано, удаляет все элементы
     * @param int $pos
     */
    public function clear($pos = null){
    	if($pos != null){
    		unset($this->list[$pos]);
    	}else{
    		$this->list = array();
    	}
    }
	
	/**
	 * Создается загрузчик элементов инфоблока. 
	 * Обязательно указывается алиас вида инфоблока и алиас самого инфоблока 
	 * @param string $code Код типа и блока, вида 'TypeCode.BlockCode'
	 * @param array $arResult Массив загруженных результатов, для стандартных компонентов
	 */
	function __construct($result = null){
		// имя класса итема получаем автоматом 
		preg_match('/(\w+)(List)$/', get_class($this), $matches);
		self::$ELEMENT_CLASS = $matches[1];
		$classVars = get_class_vars(self::$ELEMENT_CLASS);
		self::$blockCode = $classVars['blockCode'];
		self::$typeCode = $classVars['typeCode'];
		if($result != null){
			$this->setUpBitrixResult($result);
		}
	}
	
	/**
	 * Разбирает результат выборки, полученный Битриксом,
	 * и сохраняет эти данные во внутреннее представление, 
	 * создавая для каждого элемента объект класса $ELEMENT_CLASS.
	 * Например, для IssueList класс будет Issue 
	 * @param array | CIBlockResult $result
	 * @param array $fields массив подгруженных доп. свойств, 
	 * для выборки свойств вида PROPERTY_prop
	 */
	private function setUpBitrixResult($result, $fields = null) {
		if($result instanceof CIBlockResult){
			while($row = $result->GetNextElement()){
				$this->list[] = new self::$ELEMENT_CLASS($row, $fields);
			}
		}
		else{
			if(is_array($result)){
				if(isset($result['ID'])){
					$this->list[0] = new self::$ELEMENT_CLASS($result, $fields);
				}
				else{
					if(isset($result[0])){
						foreach($result as $row){
							$this->list[] = new self::$ELEMENT_CLASS($row, $fields);
						}
					}
				}
			}
		}
	}
	
	/**
	 * Получение элементов инфоблока
	 * @param array $filter
	 * @param array $order
	 * @param int | array $limit Лимит записей, либо параметры пагинации по Битриксу
	 * @param array $fields коды полей <br/> 
	 * 'fieldCode' - поле <br/> 
	 * 'PROPERTY_propCode' - свойство <br/>
	 * 'PROPERTY_propCode.childPropCode' - поля/свойства зависимого инфоблока 
	 * @param array $group
	 * @return array
	 */
	public function load(
		array $filter = array(),
		array $order = array('SORT'=>'ASC'),
		$limit = array(),
		array $fields = array(), 
		array $group = array()
	){
		$this->list = array();
		if(self::$blockCode != null){
			// дополняем фильтр
			$filter['IBLOCK_CODE'] = self::$blockCode;
		}
		$filter['ACTIVE'] = $this->active;
		
		// дополняем выбираемые поля обязательными параметрами,
		// во избежание косяков с выборками
		if(!empty($fields) && !in_array('IBLOCK_ID', $fields)){
			$fields[] = 'IBLOCK_ID';
		}
		// обрабатываем указанный лимит/параметры пагинации
		if(is_array($limit)){
			$navStartParams = $limit;
		}else{
			if(is_numeric($limit)){
				$navStartParams = array(
					'nTopCount'=>intval($limit)
				);
			}
		}
		$result = CIBlockElement::GetList($order, $filter, false, $navStartParams, $fields);
		$this->setUpBitrixResult($result, $fields);
		return $this->list;
	}
	
	
	/**
	 * Подготовка результата выборки к возвращению:
	 * - подгрузка свойств, при необходимости
	 * - подгрузка зависимых объектов, по требованию
	 * @param _CIBElement $row
	 * @param boolean | array $withProperties
	 * @param array $withChildren Массив кодов зависимых инфоблоков вида 'TypeCode.BlockCode'
	 * @return array
	 */
	public static function prepareResult(
		_CIBElement $row,
		$withProperties=false,
		$withChildren=array()
	) {
		$ret = $row->GetFields();
		if(is_array($withProperties)){
			$ret['PROPERTIES'] = $row->GetProperties(
				false,
				$withProperties
			);
		}
		else{
			if($withProperties){
				$ret['PROPERTIES'] = $row->GetProperties();
			}
		}
		//зависимые инфоблоки
		// TODO доработать, пока не используем
		if(!empty($withChildren)){
			foreach($withChildren as $childCode){
				$childBlock = new IBlockElement($childCode);
				if(isset($ret['PROPERTIES']) 
					&& isset($ret['PROPERTIES'][$childBlock->blockCode])
					&& $ret['PROPERTIES'][$childBlock->blockCode]['PROPERTY_TYPE'] == 'E'
				){
					$ret['CHILDREN'][$childCode] = $childBlock->load(
						$ret['PROPERTIES'][$childBlock->blockCode]['VALUE']
					);
				}
			}
		};
		return $ret;
	}
	
	/**
	 * Вывод RSS-ленты (канал)
	 * @param  $title Название канала
	 * @param  $link Ссылка на источник  канала
	 * @param string $description Описание канала
	 * @param int $limit сколько вернуть записей
	 * @return string UTF-текст ленты
	 */
	public function toRss($title, $link, $description = '', $limit = 20, $forceUtf = false){
		$ret = "<?xml version='1.0' encoding='utf-8'?><rss version='2.0'><channel>";
		$ret.= "<title>$title</title>";
		$ret.= sprintf('<link>%s</link>', $link);
		$ret.= "<description><![CDATA[$description]]></description>";
		$ret.= "<language>ru-ru</language>";
		$items = array_slice($this->list, 0, $limit);
		foreach($items as $item){
			$ret.= $item->toRss();
		}
		$ret.= '</channel></rss>';
		return $ret;
	}
}
